/*
 * template.c - cve-check-tool
 *
 * Copyright (C) 2015 Intel Corporation
 *
 * cve-check-tool is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#define _GNU_SOURCE

#include "template.h"
#include "util.h"

#include <string.h>

struct TemplateContext {
        gchar *name;
        struct TemplateContext *parent;
        GHashTable *values;
        GHashTable *sects;
        gchar *sect_key;
        bool emit;
        cve_string *block;
        bool in_list;
};

typedef enum {
        TC_VALUE_TYPE_MIN,
        TC_VALUE_TYPE_BOOL,   /**<Boolean type **/
        TC_VALUE_TYPE_STRING, /**<String type **/
        TC_VALUE_TYPE_LIST,   /**<List type **/
        TC_VALUE_TYPE_MAX
} TCValueType;

/**
 * Used internally to encapsulate data in a more introspectible fashion
 */
typedef struct TCValue {
        TCValueType type; /**Type of this value */
        void *value;      /**Value itself */
} TCValue;

/**
 * Remove a subcontext when we're finished with it, i.e. exiting child node
 */
static void template_context_destroy_subcontext(TemplateContext *ctx, const char *key);

/**
 * Internal functionality to add values to the table
 */
static bool template_context_add(TemplateContext *ctx, TCValueType type, const char *key, void *val);

static void template_context_value_free(TCValue *value)
{
        if (!value) {
                return;
        }
        if (value->value) {
                if (value->type == TC_VALUE_TYPE_STRING) {
                        cve_string_free(value->value);
                } else if (value->type == TC_VALUE_TYPE_LIST) {
                        g_list_free_full(value->value, (GDestroyNotify)template_context_free);
                }
        }
        free(value);
}

void template_context_free(TemplateContext *self)
{
        if (!self) {
                return;
        }
        if (self->name) {
                g_free(self->name);
        }
        if (self->values) {
                g_hash_table_unref(self->values);
        }
        if (self->sects) {
                g_hash_table_unref(self->sects);
        }
        if (self->sect_key) {
                g_free(self->sect_key);
        }
        if (self->block) {
                cve_string_free(self->block);
        }
        free(self);
}

TemplateContext *template_context_new()
{
        TemplateContext *ret = NULL;

        ret = calloc(1, sizeof(struct TemplateContext));
        ret->values =
            g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)template_context_value_free);
        ret->sects = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)template_context_free);
        ret->emit = true;
        return ret;
}

bool template_context_add_string(TemplateContext *ctx, const char *key, char *value)
{
        return template_context_add(ctx, TC_VALUE_TYPE_STRING, key, value);
}

bool template_context_add_bool(TemplateContext *ctx, const char *key, bool value)
{
        return template_context_add(ctx, TC_VALUE_TYPE_BOOL, key, GINT_TO_POINTER((gint)value));
}

bool template_context_add_list(TemplateContext *ctx, const char *key, TemplateContext *value)
{
        return template_context_add(ctx, TC_VALUE_TYPE_LIST, key, value);
}

/**
 * Add the given value of the proposed type to our internal data set, mapped to the key
 *
 * @param ctx Context to add value to
 * @param type Type of the given value
 * @param key Unique key name
 * @param val Value of the key
 *
 * @return True if the operation succeeded
 */
static bool template_context_add(TemplateContext *ctx, TCValueType type, const char *key, void *val)
{
        TCValue *value = NULL;
        TCValue *orig = NULL;

        if (!ctx || !key) {
                return false;
        }
        if (type <= TC_VALUE_TYPE_MIN || type >= TC_VALUE_TYPE_MAX) {
                return false;
        }
        if (g_hash_table_contains(ctx->values, key) && type != TC_VALUE_TYPE_LIST) {
                return false;
        }

        if (type == TC_VALUE_TYPE_LIST) {
                if (!val) {
                        return false;
                }
                orig = g_hash_table_lookup(ctx->values, key);
                if (orig) {
                        TemplateContext *tmp = val;
                        tmp->parent = ctx;
                        orig->value = g_list_append(orig->value, val);
                        return true;
                }
                orig = calloc(1, sizeof(struct TCValue));
                if (!orig) {
                        return false;
                }
                TemplateContext *tmp = val;
                tmp->parent = ctx;
                orig->type = type;
                orig->value = g_list_append(orig->value, val);
                g_hash_table_insert(ctx->values, g_strdup(key), orig);
                return true;
        }

        value = calloc(1, sizeof(struct TCValue));
        if (!value) {
                return false;
        }
        value->type = type;
        if (type == TC_VALUE_TYPE_STRING) {
                value->value = cve_string_dup(val);
        } else {
                value->value = val;
        }

        g_hash_table_insert(ctx->values, g_strdup(key), value);
        return true;
}

void template_context_add_subcontext(TemplateContext *ctx, const char *key, TemplateContext *child)
{
        if (!ctx || !child) {
                return;
        }
        if (g_hash_table_contains(ctx->sects, key)) {
                return;
        }
        child->parent = ctx;
        child->name = g_strdup(key);
        g_hash_table_insert(ctx->sects, child->name, child);
}

static void template_context_destroy_subcontext(TemplateContext *ctx, const char *key)
{
        if (!ctx || !key) {
                return;
        }
        if (!g_hash_table_contains(ctx->sects, key)) {
                return;
        }
        g_hash_table_remove(ctx->sects, key);
}

/**
 * Find a value for the key within ctx, if it doesn't exist, check through
 * all of its parents
 */
static inline TCValue *find_context_value(TemplateContext *ctx, const char *key)
{
        __attribute__((unused)) TCValue *val = NULL;
        if (!ctx || !key) {
                return NULL;
        }

        val = g_hash_table_lookup(ctx->values, key);
        if (!val && ctx->parent) {
                /* Search backwards in context stack */
                TemplateContext *search = ctx;
                while ((search = search->parent)) {
                        val = g_hash_table_lookup(search->values, key);
                        if (val) {
                                return val;
                        }
                }
        }
        return val;
}

/**
 * Find an appropriate TemplateContext, or create one if needed.
 *
 * This enables conditionals for null-checks right now, but in future
 * will be expanded to enable iterables, and the subcontext handling
 * will likely become private, switching to a type based API
 */
static inline TemplateContext *get_context(TemplateContext *ctx, const char *key)
{
        __attribute__((unused)) TemplateContext *val = NULL;
        __attribute__((unused)) TCValue *tval = NULL;
        if (!ctx || !ctx->sects || !key) {
                return NULL;
        }
        if ((val = g_hash_table_lookup(ctx->sects, key))) {
                return val;
        }
        tval = find_context_value(ctx, key);
        if (tval) {
                /* For a native boolean we can choose to ignore this block */
                if (tval->type == TC_VALUE_TYPE_BOOL && !tval->value) {
                        return NULL;
                }
                TemplateContext *cctx = template_context_new();
                template_context_add_subcontext(ctx, key, cctx);
                return cctx;
        }
        return NULL;
}

static inline void insert_missing(TemplateContext *context, cve_string **str)
{
        /* Respect section visibility */
        if (!context || !context->emit) {
                return;
        }
        if (str && *str) {
                cve_string_cat(*str, "{");
        } else {
                *str = cve_string_dup("{");
        }
}

static inline bool within_list(TemplateContext *self)
{
        TemplateContext *ctx = self;
        while (ctx) {
                if (ctx->in_list) {
                        return true;
                }
                ctx = ctx->parent;
        }
        return false;
}

cve_string *template_context_process_line(TemplateContext *self, const char *original, bool skip)
{
        char *c = NULL, *s = NULL;
        int offset = 0;
        TemplateContext *ctx = self;
        cve_string *input;

        input = cve_string_dup(original);

        if (!input || !ctx || !ctx->values) {
                /* Always return allocated string for consistency */
                return input;
        }

        while ((c = memchr(input->str + offset, '{', input->len - offset))) {
                autofree(cve_string) *newstr = NULL;
                TCValue *val = NULL;

                int oldoffset = offset;
                offset = (c - input->str);

                if (ctx->emit && offset - oldoffset >= 1) {
                        char *strstart = input->str + oldoffset;
                        strstart[offset - oldoffset] = '\0';

                        if (!ctx->block) {
                                ctx->block = cve_string_dup(strstart);
                        } else {
                                cve_string_cat(ctx->block, strstart);
                        }
                }

                if (*(c + 1) != '{') {
                        insert_missing(ctx, &ctx->block);
                        ++offset;
                        goto bail;
                }
                s = c;
                c = memchr(c, '}', input->len);
                if (!c) {
                        insert_missing(ctx, &ctx->block);
                        ++offset;
                        goto bail;
                }
                if (*(c + 1) != '}') {
                        insert_missing(ctx, &ctx->block);
                        ++offset;
                        goto bail;
                }

                int start = (s - input->str);
                start += 2;
                int end = (c - input->str);
                int length = end - start;

                newstr = cve_string_dup(input->str + start);
                newstr->str[length] = '\0';
                newstr->len = length;

                offset += length + 4;
                if (newstr->str[0] == '#') {
                        TemplateContext *child = get_context(ctx, newstr->str + 1);
                        if (ctx->sect_key) {
                                g_free(ctx->sect_key);
                        }
                        ctx->sect_key = g_strdup(newstr->str + 1);
                        if (!child) {
                                ctx->emit = false;
                        } else {
                                ctx = child;
                                ctx->emit = true;
                                TCValue *val = find_context_value(ctx->parent, ctx->name);
                                if (val && val->type == TC_VALUE_TYPE_LIST) {
                                        ctx->parent->in_list = true;
                                }
                        }
                } else if (newstr->str[0] == '/') {
                        gchar *sect = newstr->str + 1;
                        if (ctx->name && g_str_equal(sect, ctx->name)) {
                                if (ctx->block) {
                                        autofree(cve_string) *contents = ctx->block;
                                        cve_string *complete;
                                        ctx->block = NULL;
                                        /* Search parent for the value in question */
                                        TCValue *val = find_context_value(ctx->parent, ctx->name);
                                        if (val && val->type == TC_VALUE_TYPE_LIST) {
                                                GList *root = val->value, *child = NULL;
                                                for (child = root; child; child = child->next) {
                                                        TemplateContext *cctx = child->data;
                                                        complete =
                                                            template_context_process_line(cctx, contents->str, true);
                                                        if (!ctx->parent->block) {
                                                                ctx->parent->block = complete;
                                                        } else {
                                                                cve_string_cat(ctx->parent->block, complete->str);
                                                                cve_string_free(complete);
                                                        }
                                                }
                                        } else {
                                                /* Normal textual content */
                                                complete = template_context_process_line(ctx, contents->str, true);
                                                if (!ctx->parent->block) {
                                                        ctx->parent->block = complete;
                                                } else {
                                                        cve_string_cat(ctx->parent->block, complete->str);
                                                        cve_string_free(complete);
                                                }
                                        }
                                }
                                ctx = ctx->parent;
                                ctx->in_list = false;
                        } else {
                                if (ctx->sect_key && !g_str_equal(ctx->sect_key, sect)) {
                                        g_warning("Ending section without starting one: %s (ctx: %s)",
                                                  sect,
                                                  ctx->sect_key);
                                        return NULL;
                                }
                        }
                        if (ctx->sect_key) {
                                g_free(ctx->sect_key);
                                ctx->sect_key = NULL;
                        }
                        template_context_destroy_subcontext(ctx, sect);
                        ctx->emit = true;
                } else {
                        /* Do we render this section or not */
                        if (ctx->parent && !skip) {
                                if (ctx->emit) {
                                        autofree(gchar) *key = g_strdup_printf("{{%s}}", newstr->str);
                                        if (!ctx->block) {
                                                ctx->block = cve_string_dup(key);
                                        } else {
                                                cve_string_cat(ctx->block, key);
                                        }
                                }
                        } else {
                                /* Either forced render on second pass or root node */
                                if (ctx->emit) {
                                        val = find_context_value(ctx, newstr->str);
                                        if (val) {
                                                if (!ctx->block) {
                                                        if (val->type == TC_VALUE_TYPE_STRING) {
                                                                ctx->block =
                                                                    cve_string_dup(((cve_string *)val->value)->str);
                                                        } else if (val->type == TC_VALUE_TYPE_BOOL) {
                                                                ctx->block =
                                                                    cve_string_dup(val->value ? "true" : "false");
                                                        } else {
                                                                g_warning(
                                                                    "Cannot render value of '%s' - which is a list",
                                                                    newstr->str);
                                                        }
                                                } else {
                                                        if (val->type == TC_VALUE_TYPE_STRING) {
                                                                cve_string_cat(ctx->block,
                                                                               ((cve_string *)val->value)->str);
                                                        } else if (val->type == TC_VALUE_TYPE_BOOL) {
                                                                cve_string_cat(ctx->block,
                                                                               val->value ? "true" : "false");
                                                        } else {
                                                                g_warning(
                                                                    "Cannot render value of '%s' - which is a list",
                                                                    newstr->str);
                                                        }
                                                }
                                        } else {
                                                if (within_list(ctx)) {
                                                        /* Simply cat the string back for recheck later up the chain */
                                                        autofree(gchar) *key = g_strdup_printf("{{%s}}", newstr->str);
                                                        if (!ctx->block) {
                                                                ctx->block = cve_string_dup(key);
                                                        } else {
                                                                cve_string_cat(ctx->block, key);
                                                        }
                                                }
                                        }
                                }
                        }
                }
        bail:
                if (offset >= input->len) {
                        break;
                }
        }

        if (offset < input->len) {
                if (!ctx->block) {
                        ctx->block = cve_string_dup(input->str + offset);
                } else {
                        cve_string_cat(ctx->block, input->str + offset);
                }
        }

        cve_string_free(input);

        if (ctx->block) {
                input = ctx->block;
                ctx->block = NULL;
        } else {
                input = NULL;
        }

        return input;
}

cve_string *template_string(const char *original, GHashTable *keys)
{
        autofree(TemplateContext) *context = NULL;
        GHashTableIter iter;
        gchar *key = NULL, *value = NULL;

        if (!original) {
                return NULL;
        }

        if (!keys) {
                return cve_string_dup(original);
        }

        context = template_context_new();

        g_hash_table_iter_init(&iter, keys);
        while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) {
                template_context_add_string(context, key, value);
        }

        return template_context_process_line(context, original, false);
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 expandtab:
 * :indentSize=8:tabSize=8:noTabs=true:
 */
